Skip to content

fix(core): give readWorkspace ignore rules, size caps and a skip report - #24

Merged
amondnet merged 2 commits into
mainfrom
fix/workspace-ignore-rules
Aug 28, 2026
Merged

fix(core): give readWorkspace ignore rules, size caps and a skip report#24
amondnet merged 2 commits into
mainfrom
fix/workspace-ignore-rules

Conversation

@amondnet

@amondnet amondnet commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

readWorkspace walked the source with a bare recursive readdir and read every file into a string. There was no ignore list and no size cap, so pointing workspace at a real Claude Code project walked node_modules, .git and every build artifact into memory — and since the UTF-8 guard landed, one .git pack or icon among them failed the entire seed. The use case workspace.ts's own header and README.md state as the reason it exists could not run at all.

Ignore rules come from git. git ls-files -co --exclude-standard -z with cwd at the source root is exactly the tracked plus untracked-not-ignored set: the project's own maintained .gitignore decides what travels, and there is no gitignore parser of ours to disagree with git about. -z so a filename containing a newline survives; the paths come out relative to the cwd, which is the shape WorkspaceFiles keys already use.

A non-git source falls back to a short built-in deny list (WORKSPACE_IGNORED_DIRECTORIES: .git, node_modules, dist, build, coverage, .next, .turbo, .cache, out). It is a fallback, not the primary mechanism.

A binary that survives the ignore rules is skipped and reported, not fatal. Before the rules existed, refusing was right — the file had only been swept up by a broad walk. After them it is real project content that cannot cross a text-only contract, and failing the seed over an icon helps nobody. decodeText's detection is unchanged; only what happens on failure is.

Size caps are internal constants, not new options. 1 MiB per file is reported like a binary; 64 MiB in total throws, because a seed that large is a mistake about which directory was handed over.

node:child_process is reached through a dynamic import, exactly as node:fs and node:url already are, so a Worker bundle that only ever passes an inlined record does not pull the host process surface in. The WorkspaceFiles input path still short-circuits before any of it.

Breaking change

readWorkspace now returns { files, skipped } instead of WorkspaceFiles; skipped is { path, reason: 'binary' | 'too-large' }[]. "Reported" has to mean observable, and a callback would put logging in a package that has no logger. defineAgent is updated; seedWorkspace still takes WorkspaceFiles and is unaffected.

Related issue

Closes #19

Checklist

  • PR title follows Conventional Commits
  • Tests added or updated, and the suite passes (bun run test) — 206 pass, up from 202
  • Lint and type-check pass (bun run lint, bun run type-check)
  • Documentation updated if behavior changed — the module's own prose comments
  • No breaking change, or a BREAKING CHANGE: note is included

Summary by cubic

Fixes readWorkspace so pointing it at a real project no longer sweeps node_modules, .git, and build artifacts into memory, and one binary file no longer fails the entire seed. It now honors the project's own git ignore rules (git ls-files -co --exclude-standard), falls back to a short built-in deny list when the source is not a git repo, and skips and reports binary or oversized files instead of throwing. Size caps (1 MiB per file, 64 MiB total) are enforced, and two failures raise instead of degrading: a repo whose file list overruns the buffer, and a listed file that cannot be stat-ed for any reason other than absence.

Breaking change

Written for commit 1c87e47. Summary will update on new commits.

Pointing `workspace` at a real Claude Code project walked `node_modules`,
`.git` and every build artifact into memory, and since the UTF-8 guard
landed one `.git` pack or icon among them failed the entire seed — so the
use case this file's own header states as the reason it exists could not
run at all.

Ignore rules come from git: `git ls-files -co --exclude-standard -z` in
the source root is exactly the tracked plus untracked-not-ignored set,
which makes the project's own maintained `.gitignore` the source of truth
and leaves us no gitignore parser to disagree with git about. A source
git cannot describe falls back to a short built-in deny list.

A binary that survives the ignore rules is now skipped and reported
rather than fatal. Before the rules existed, refusing was right because
the file had only been swept up by a broad walk; after them it is real
project content — an icon, a font — that simply cannot cross a text-only
contract, and failing the seed over it helps nobody.

Size caps are internal constants, not options nobody has asked for: 1 MiB
per file is reported like a binary, and 64 MiB in total throws, because a
seed that large is a mistake about which directory was handed over and
silence would be worse than a stop.

BREAKING CHANGE: `readWorkspace` returns `{ files, skipped }` rather than
`WorkspaceFiles`. Reporting a skipped file has to mean something the
caller can observe, and this package has no logger to write it to.
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@amondnet
amondnet marked this pull request as ready for review August 28, 2026 18:30

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Architecture diagram
sequenceDiagram
    participant Agent as defineAgent
    participant RW as readWorkspace
    participant Git as git CLI
    participant FS as node:fs/promises
    participant CP as node:child_process
    participant Seed as seedWorkspace

    Note over Agent,RW: Workspace resolution flow
    Agent->>RW: readWorkspace(source)
    
    alt Source is inlined WorkspaceFiles
        RW-->>Agent: { files: source, skipped: [] }
    else Source is path or URL
        RW->>RW: Resolve root (URL -> fileURLToPath)
        RW->>Git: git ls-files -co --exclude-standard -z (cwd=root)
        alt Git succeeds
            Git-->>RW: Relative file candidates
        else Git fails (no repo, no git, unsafe dir)
            RW->>FS: readdir(root, { recursive: true })
            FS-->>RW: All entries
            RW->>RW: Filter by WORKSPACE_IGNORED_DIRECTORIES
        end
        
        RW->>FS: stat each candidate
        loop For each surviving file
            alt File missing or not regular
                FS-->>RW: Skip silently
            else File size > MAX_WORKSPACE_FILE_BYTES
                RW->>RW: Record skipped { path, reason: 'too-large' }
            else Total accumulated > MAX_WORKSPACE_TOTAL_BYTES
                RW-->>Agent: Throw RangeError
            else File decodes as valid UTF-8
                FS-->>RW: Read bytes
                RW->>RW: decodeText() with fatal TextDecoder
                RW->>RW: Store in files[normalizedPath]
            else Decode fails (binary content)
                RW->>RW: Record skipped { path, reason: 'binary' }
            end
        end
        RW-->>Agent: { files, skipped }
    end
    
    Agent->>Seed: seedWorkspace(session, sessionWorkDir, files)
    Seed-->>Agent: Seeded workspace files
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/src/agent/workspace.ts Outdated
Comment thread packages/core/src/agent/workspace.ts
…degrading

`gitCandidates` treated every `git ls-files` failure as "not a repository". An output
limit is not that: `execFile` rejects with `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` when a
real repository's file list overruns `maxBuffer`, and the fallback walk honours no
`.gitignore` — so the guard added to keep `node_modules` out was the one path that
would start carrying it. That error now raises.

`readCandidates` dropped any path `stat` refused. Only a vanished entry is nothing to
read; a permission denial is a file the caller asked for and did not get, so it is
raised rather than dropped into the silence the `skipped` report exists to remove.
@amondnet
amondnet added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit e6bf482 Aug 28, 2026
7 checks passed
@amondnet
amondnet deleted the fix/workspace-ignore-rules branch August 28, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

readWorkspace has no ignore rules or size cap, so it cannot carry a real project

1 participant